/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2010  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

/*
 * ``Provider Service'', a service that piggy-backs on top of
 * a real transport service.  Right now this is only used for
 * VPN support.
 *
 * Providers are created via Manager.GetVPNService.  This creates
 * a provider instance and an associated service that is bound to
 * the provider.  Provider callbacks handles connect/disconnect work
 * such as startup of an external application and plumbing of traffic
 * through (an optional) device that's created on the fly (e.g. a
 * tun device by which the vpn process handles IPSec).  Providers
 * are visible externally only though properties on the Service
 * object (which are enabled only when the service is operational).
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <gdbus.h>

#include <connman/assert.h>

#include "connman.h"

/* TODO(sleffler) re-use vpn since that's all this is used for right now */
#define	_DBG_PROVIDER(fmt, arg...)	DBG(DBG_VPN | DBG_SERVICE, fmt, ## arg)

static DBusConnection *connection = NULL;
static GHashTable *provider_hash = NULL;	/* private hash by identifier */
static GSList *driver_list = NULL;		/* provider drivers */

struct connman_provider {
	struct connman_element element;
	struct connman_service *vpn_service;
	struct connman_ipconfig *ipv4config;
	enum connman_provider_state state;
	char *identifier;
	char *interface;
	struct connman_profile *profile;
	struct connman_provider_driver *driver;
	void *driver_data;
};

static struct {
	const char *provider_tag;	/* name recorded in provider/element */
	const char *keyfile_tag;	/* name recorded in GKeyFile/profile */
} provider_tags[] = {
	{ "Name",	CONNMAN_PROVIDER_NAME },
	{ "Host",	CONNMAN_PROVIDER_HOST },
	{ "Type",	CONNMAN_PROVIDER_TYPE },
	{ "VPN.Domain",	CONNMAN_VPN_DOMAIN },
};
#define	PROVIDER_NTAGS	(sizeof(provider_tags) / sizeof(provider_tags[0]))

static void __connman_provider_ipconfig_init(struct connman_provider *provider);

static const char *__statename(enum connman_provider_state state)
{
    static const char *statenames[] = {
        [CONNMAN_PROVIDER_STATE_UNKNOWN]     = "UNKNOWN",
        [CONNMAN_PROVIDER_STATE_IDLE]        = "IDLE",
        [CONNMAN_PROVIDER_STATE_CONNECT]     = "CONNECT",
        [CONNMAN_PROVIDER_STATE_READY]       = "READY",
        [CONNMAN_PROVIDER_STATE_DISCONNECT]  = "DISCONNECT",
        [CONNMAN_PROVIDER_STATE_FAILURE]     = "FAILURE",
    };
    return statenames[state];
}

/*
 * Append object paths for each ipconfig associated with a provider.
 */
static void append_ipconfig_path(struct connman_ipconfig *ipconfig, void *arg)
{
	const char *str = connman_ipconfig_get_path(ipconfig);
	DBusMessageIter *iter = arg;

	dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &str);
}
static void append_ipconfig(DBusMessageIter *dict, void *arg)
{
	struct connman_provider *provider = arg;

	__connman_ipconfig_foreach(provider, append_ipconfig_path, dict);
}

/*
 * Export visible provider properties given a provider object.
 * The properties are wrapped in a Provider dictionary and use
 * shortened keys (e.g. Type instead of Provider.Type).
 */
void __connman_provider_append_properties(DBusMessageIter *iter, void *arg)
{
	struct __connman_provider_append_properties_args *args = arg;
	struct connman_provider *provider = args->provider;
	int i;

	for (i = 0; i < PROVIDER_NTAGS; i++) {
		const char *str = connman_element_get_string(&provider->element,
		    provider_tags[i].keyfile_tag);
		if (str != NULL)
			connman_dbus_dict_append_basic(iter,
			    provider_tags[i].provider_tag,
			    DBUS_TYPE_STRING, &str);
	}
	if (provider->interface != NULL)
		connman_dbus_dict_append_basic(iter, "Interface",
		    DBUS_TYPE_STRING, &provider->interface);

	connman_dbus_dict_append_variant_array(iter, "IPConfigs",
	    DBUS_TYPE_OBJECT_PATH, append_ipconfig, provider);

	if (provider->driver != NULL &&
	    provider->driver->append_props != NULL) {
		provider->driver->append_props(provider, iter,
		    __connman_security_check_privilege(args->msg,
		        CONNMAN_SECURITY_PRIVILEGE_READ_SECRET) == 0);
	}
}

/*
 * Export provider properties stored in a profile.
 * The properties are wrapped in a Provider dictionary and use
 * shortened keys (e.g. Type instead of Provider.Type).
 */
void __connman_provider_append_profile_properties(DBusMessageIter *dict,
    void *arg)
{
	struct __connman_provider_append_profile_properties_args *args = arg;
	int i;

	for (i = 0; i < PROVIDER_NTAGS; i++) {
		char *str = g_key_file_get_string(args->keyfile,
		    args->ident, provider_tags[i].keyfile_tag, NULL);
		if (str != NULL) {
			connman_dbus_dict_append_variant(dict,
			    provider_tags[i].provider_tag,
			    DBUS_TYPE_STRING, &str);
			g_free(str);
		}
	}
}

static struct connman_provider *connman_provider_lookup(const char *identifier)
{
	struct connman_provider *provider = NULL;

	provider = g_hash_table_lookup(provider_hash, identifier);

	return provider;
}

struct connman_provider *connman_provider_ref(struct connman_provider *provider)
{
	_DBG_PROVIDER("provider %p", provider);

	if (connman_element_ref(&provider->element) == NULL)
		return NULL;

	return provider;
}

void connman_provider_unref(struct connman_provider *provider)
{
	_DBG_PROVIDER("provider %p refcount %d", provider,
	    g_atomic_int_get(&provider->element.refcount) - 1);

	connman_element_unref(&provider->element);
}

static gboolean match_driver(struct connman_provider *provider,
				struct connman_provider_driver *driver)
{
	const char *type = connman_element_get_string(&provider->element,
	    CONNMAN_PROVIDER_TYPE);
	return (g_strcmp0(driver->name, type) == 0);
}

static int provider_probe(struct connman_provider *provider)
{
	GSList *list;

	_DBG_PROVIDER("provider %p name %s", provider,
	    connman_element_get_string(&provider->element, "Name"));

	if (provider->driver != NULL)
		return -EALREADY;

	for (list = driver_list; list; list = list->next) {
		struct connman_provider_driver *driver = list->data;

		if (match_driver(provider, driver) == FALSE)
			continue;

		_DBG_PROVIDER("driver %p name %s", driver, driver->name);

		if (driver->probe != NULL && driver->probe(provider) == 0) {
			provider->driver = driver;
			break;
		}
	}

	if (provider->driver == NULL)
		return -ENODEV;

	return 0;
}

int __connman_provider_disconnect(struct connman_provider *provider)
{
	_DBG_PROVIDER("provider %p", provider);

	if (provider->driver == NULL || provider->driver->disconnect == NULL)
		return -EOPNOTSUPP;

	return provider->driver->disconnect(provider);
}

int __connman_provider_connect(struct connman_provider *provider)
{
	int err;

	_DBG_PROVIDER("provider %p", provider);

	if (provider->driver == NULL || provider->driver->connect == NULL)
		return -EOPNOTSUPP;

	err = provider->driver->connect(provider);
	if (err < 0) {
		if (err != -EINPROGRESS)
			return err;

		__connman_service_indicate_state(provider->vpn_service,
					CONNMAN_SERVICE_STATE_ASSOCIATION);
		return -EINPROGRESS;
	}

	return 0;
}

void __connman_provider_remove(struct connman_provider *provider)
{
	_DBG_PROVIDER("provider %s", provider->identifier);

	g_hash_table_remove(provider_hash, provider->identifier);
	connman_provider_unref(provider);
}

int connman_provider_set_state(struct connman_provider *provider,
					enum connman_provider_state state)
{
	static const enum connman_service_state statemap[] = {
	    [CONNMAN_PROVIDER_STATE_IDLE]	= CONNMAN_SERVICE_STATE_IDLE,
	    [CONNMAN_PROVIDER_STATE_DISCONNECT] =
	        CONNMAN_SERVICE_STATE_DISCONNECT,
	    [CONNMAN_PROVIDER_STATE_FAILURE]	= CONNMAN_SERVICE_STATE_FAILURE,
	};

	if (provider == NULL || provider->vpn_service == NULL)
		return -EINVAL;

	_DBG_PROVIDER("state change (%p) %s -> %s", provider,
		__statename(provider->state), __statename(state));
	provider->state = state;

	/* TODO(sleffler) check return values */
	switch (state) {
	case CONNMAN_PROVIDER_STATE_UNKNOWN:
		return -EINVAL;
	case CONNMAN_PROVIDER_STATE_IDLE:
	case CONNMAN_PROVIDER_STATE_DISCONNECT:
	case CONNMAN_PROVIDER_STATE_FAILURE:
		(void) __connman_service_indicate_state(provider->vpn_service,
		    statemap[state]);
		connman_provider_ipconfig_clear(provider);
		break;
	case CONNMAN_PROVIDER_STATE_CONNECT:
		(void) __connman_service_indicate_state(provider->vpn_service,
		    CONNMAN_SERVICE_STATE_ASSOCIATION);
		break;
	case CONNMAN_PROVIDER_STATE_READY:
		(void) __connman_service_indicate_state(provider->vpn_service,
		    CONNMAN_SERVICE_STATE_CONFIGURATION);
		/* we have the L3 state, bind directly */
		connman_provider_ipconfig_bind(provider);
		break;
	}
	return 0;
}

int connman_provider_indicate_error(struct connman_provider *provider,
					enum connman_provider_error error)
{
	static const enum connman_service_error errmap[] = {
	    [CONNMAN_PROVIDER_ERROR_NO_ERROR] =
	        CONNMAN_SERVICE_ERROR_NO_ERROR,
	    [CONNMAN_PROVIDER_ERROR_PIN_MISSING] =
	        CONNMAN_SERVICE_ERROR_PIN_MISSING,
	    [CONNMAN_PROVIDER_ERROR_CONNECT_FAILED] =
	        CONNMAN_SERVICE_ERROR_CONNECT_FAILED,
	    [CONNMAN_PROVIDER_ERROR_BAD_PASSPHRASE] =
	        CONNMAN_SERVICE_ERROR_BAD_PASSPHRASE,
	    [CONNMAN_PROVIDER_ERROR_RESOLVE_HOSTNAME_FAILED] =
	        CONNMAN_SERVICE_ERROR_DNS_LOOKUP_FAILED,
	    [CONNMAN_PROVIDER_ERROR_IPSEC_PSK_AUTH_FAILED] =
	        CONNMAN_SERVICE_ERROR_IPSEC_PSK_AUTH_FAILED,
	    [CONNMAN_PROVIDER_ERROR_IPSEC_CERT_AUTH_FAILED] =
	        CONNMAN_SERVICE_ERROR_IPSEC_CERT_AUTH_FAILED,
	    [CONNMAN_PROVIDER_ERROR_PPP_AUTH_FAILED] =
	        CONNMAN_SERVICE_ERROR_PPP_AUTH_FAILED,
	    [CONNMAN_PROVIDER_ERROR_INTERNAL] =
	        CONNMAN_SERVICE_ERROR_INTERNAL,
	};

	if (provider == NULL || provider->vpn_service == NULL)
		return -EINVAL;

	_DBG_PROVIDER("provider %p error %d", provider, error);

	(void) __connman_service_indicate_error(provider->vpn_service,
	    errmap[error]);
	provider->state = CONNMAN_PROVIDER_STATE_FAILURE;
	connman_provider_ipconfig_clear(provider);

	return 0;
}

static void unregister_provider(gpointer data)
{
	struct connman_provider *provider = data;

	_DBG_PROVIDER("provider %p", provider);

	provider->vpn_service = NULL;

	/* NB: also drops refcnt */
	connman_element_unregister(&provider->element);
}

static void provider_destruct(struct connman_element *element)
{
	struct connman_provider *provider = element->private;

	_DBG_PROVIDER("provider %p", provider);

	connman_provider_ipconfig_clear(provider);

	g_free(provider->identifier);
	g_free(provider->interface);
}

static struct connman_provider *connman_provider_new(void)
{
	struct connman_provider *provider;

	provider = g_try_new0(struct connman_provider, 1);
	if (provider == NULL)
		return NULL;

	_DBG_PROVIDER("provider %p", provider);

	__connman_element_initialize(&provider->element);

	provider->element.type = CONNMAN_ELEMENT_TYPE_PROVIDER;
	provider->element.private = provider;
	provider->element.destruct = provider_destruct;

	provider->state = CONNMAN_PROVIDER_STATE_UNKNOWN;

	return provider;
}

static struct connman_provider *connman_provider_get(const char *identifier)
{
	struct connman_provider *provider;

	provider = g_hash_table_lookup(provider_hash, identifier);
	if (provider != NULL)
		return provider;

	provider = connman_provider_new();
	if (provider == NULL)
		return NULL;

	_DBG_PROVIDER("provider %p", provider);

	provider->identifier = g_strdup(identifier);

	__connman_profile_load_provider(provider);

	g_hash_table_insert(provider_hash, provider->identifier, provider);

	provider->element.name = g_strdup(identifier);
	connman_element_register(&provider->element, NULL);

	return provider;
}

static void provider_dbus_ident(char *ident)
{
	int i, len = strlen(ident);

	for (i = 0; i < len; i++) {
		if (ident[i] >= '0' && ident[i] <= '9')
			continue;
		if (ident[i] >= 'a' && ident[i] <= 'z')
			continue;
		if (ident[i] >= 'A' && ident[i] <= 'Z')
			continue;
		ident[i] = '_';
	}
}

/*
 * Create a provider and associated service if not already present.
 * Returns the DBus reply message on error; otherwise NULL. This is
 * used to implement the Manager.GetService, Manager.ConfigureService,
 * and Manager.GetVPNService methods when no GUID is used to lookup
 * an existing service object.
 */
DBusMessage *__connman_provider_create(DBusMessage *msg,
    struct connman_service **service_out,
    connman_bool_t *created_out)
{
#define	iskey(a, b)	(g_str_equal((a), (b)) == TRUE)
	struct connman_provider *provider;
	DBusMessageIter iter, array;
	const char *type = NULL, *name = NULL;
	const char *host = NULL, *domain = NULL;
	char *ident, *str;
	gboolean created = FALSE;
	int err;

	dbus_message_iter_init(msg, &iter);
	dbus_message_iter_recurse(&iter, &array);

	while (dbus_message_iter_get_arg_type(&array) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(&array, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		switch (dbus_message_iter_get_arg_type(&value)) {
		case DBUS_TYPE_STRING:
			dbus_message_iter_get_basic(&value, &str);
			if (iskey(key, CONNMAN_PROVIDER_TYPE))
				type = str;
			else if (iskey(key, CONNMAN_PROVIDER_NAME))
				name = str;
			else if (iskey(key, CONNMAN_PROVIDER_HOST))
				host = str;
			else if (iskey(key, CONNMAN_VPN_DOMAIN))
				domain = str;
			/* NB: accept Type, Name, Host for compat */
			else if (iskey(key, "Type") == TRUE) {
				if (type == NULL)
					type = str;
			} else if (iskey(key, "Name") == TRUE) {
				if (name == NULL)
					name = str;
			} else if (iskey(key, "Host") == TRUE) {
				if (host == NULL)
					host = str;
			}
			break;
		}

		dbus_message_iter_next(&array);
	}

	_DBG_PROVIDER("type %s name %s host %s domain %s",
	    type, name, host, domain);

	if (host == NULL || domain == NULL) {
		err = -EINVAL;
		goto failed;
	}

	if (type == NULL || name == NULL) {
		err = -EOPNOTSUPP;
		goto failed;
	}

	ident = g_strdup_printf("vpn_%s_%s", host, name);
	provider_dbus_ident(ident);

	_DBG_PROVIDER("ident %s", ident);

	provider = connman_provider_lookup(ident);
	if (provider == NULL) {
		created = TRUE;
		provider = connman_provider_get(ident);
	}
	g_free(ident);
	ident = NULL;

	if (provider == NULL) {
		connman_error("%s: can not create provider", __func__);
		err = -EOPNOTSUPP;
		goto failed;
	}

	if (created == TRUE) {
		/*
		 * Install properties required for probing; these are
		 * blindly overwritten below which should be ok and not
		 * too expensive to avoid.
		 */
		connman_provider_set_string(provider, CONNMAN_PROVIDER_TYPE,
		   type);
		/*
		 * The Name property needs to be set here as it will be
		 * later checked by __connman_service_create_from_provider,
		 * and before service_register gets a chance to set it.
		 */
		connman_provider_set_string(provider, CONNMAN_PROVIDER_NAME,
		   name);
		provider_probe(provider);
	}

	__connman_profile_load_provider(provider);

	if (created == TRUE)
		__connman_profile_save_provider(provider);

	if (provider->vpn_service == NULL)
		provider->vpn_service =
			__connman_service_create_from_provider(provider);
	if (provider->vpn_service == NULL) {
		err = -EOPNOTSUPP;
		goto failed;
	}

	if (created_out != NULL)
		*created_out = created;
	if (service_out != NULL)
		*service_out = provider->vpn_service;
	return NULL;

failed:
	if (provider != NULL && created == TRUE)
		connman_provider_unref(provider);
	return __connman_error_failed(msg, -err);
#undef iskey
}

/*
 * Create a provider and associated service (if not already present)
 * given just the identifier.  Return a DBus reply message on error;
 * otherwise NULL.  This is used to implement the Manager.GetService
 * and Manager.ConfigureService methods for a VPN service where the
 * GUID is used to lookup the service object.
 */
DBusMessage *__connman_provider_create_by_identifier(DBusMessage *msg,
    const char *ident)
{
	struct connman_provider *provider;

	_DBG_PROVIDER("ident %s", ident);

	provider = connman_provider_lookup(ident);
	if (provider != NULL) {
		/* NB: should not happen */
		connman_error("%s: provider already exists", __func__);
		return __connman_error_already_exists(msg);
	}

	provider = connman_provider_get(ident);
	if (provider == NULL) {
		connman_error("%s: can not create provider", __func__);
		return __connman_error_failed(msg, ENOMEM);
	}

	/*
	 * Load the provider properties from the profile.  Note that we
	 * must do this twice because we need the provider type to probe
	 * for the associated driver--only the driver can read in properties
	 * it manages.  This isn't necessary when the type and name come
	 * in via the d-bus...
	 */
	__connman_profile_load_provider(provider);
	/* TODO(sleffler) verify driver attached */
	provider_probe(provider);
	__connman_profile_load_provider(provider);

	/*
	 * Finally bind the service and provider objects.  We
	 * are certain vpn_service is NULL because we created a
	 * new provider instance.
	 */
	provider->vpn_service =
	    __connman_service_create_from_provider(provider);
	if (provider->vpn_service == NULL) {
		connman_provider_unref(provider);
		return __connman_error_internal_error(msg);
	}

	return NULL;
}

const char *connman_provider_get_ident(struct connman_provider *provider)
{
	return (provider == NULL) ? NULL : provider->identifier;
}

int connman_provider_set_string(struct connman_provider *provider,
					const char *key, const char *value)
{
	_DBG_PROVIDER("provider %p key %s value %s", provider, key,
		      connman_log_get_masked_value(key, value));

	return connman_element_set_string(&provider->element, key, value);
}

DBusMessage *__connman_provider_set_property(struct connman_provider *provider,
    DBusMessage *msg, const char *name, DBusMessageIter *value)
{
	const char *cp, *str;

	cp = strchr(name, '.');
	if (cp == NULL) {
		if (g_str_equal(name, "Name") == TRUE ||
		    g_str_equal(name, "Type") == TRUE ||
		    g_str_equal(name, "Host") == TRUE) {
			/* NB: drop compat properties for get_vpn_service */
		    	return NULL;
		}
		return __connman_error_invalid_property(msg);
	}

	if (g_ascii_strncasecmp(name, "provider", cp-name) != 0 &&
	    g_ascii_strncasecmp(name, "vpn", cp-name) != 0 &&
	    (provider->driver != NULL &&
	     g_ascii_strncasecmp(name, provider->driver->name, cp-name) != 0))
		return __connman_error_invalid_property(msg);

	if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_STRING)
		return __connman_error_invalid_arguments(msg);

	dbus_message_iter_get_basic(value, &str);
	if (connman_provider_set_string(provider, name, str) != 0)
		return __connman_error_failed(msg, 0/* "Failed" */);

	__connman_profile_save_provider(provider);

	return NULL;		/* NB: always success */
}

connman_bool_t connman_provider_property_is_empty(
    struct connman_provider *provider, const char *property) {
	const char *value = connman_provider_get_string(provider, property);
	return (value == NULL || value[0] == '\0') ? TRUE : FALSE;
}

void __append_property(struct connman_provider *provider,
			const char *key, DBusMessageIter *iter)
{
	const char *str = connman_provider_get_string(provider, key);

	if (str != NULL)
		connman_dbus_dict_append_basic(iter, key, DBUS_TYPE_STRING,
		    &str);
}

void connman_provider_append_properties(struct connman_provider *provider,
					const char *props[],
					DBusMessageIter *iter)
{
	int i;

	for (i = 0; props[i] != NULL; i++)
		__append_property(provider, props[i], iter);
}

void connman_provider_load_save_properties(
    struct connman_provider *provider,
    const char *props[],
    void (*cb)(struct connman_provider *, const char *, GKeyFile *),
    GKeyFile *keyfile)
{
	int i;

	for (i = 0; props[i] != NULL; i++)
		cb(provider, props[i], keyfile);
}

void connman_provider_save_property(struct connman_provider *provider,
					const char *key, GKeyFile *keyfile)
{
	const char *str = connman_provider_get_string(provider, key);

	if (str != NULL)
		g_key_file_set_string(keyfile, provider->identifier, key, str);
}

void connman_provider_save_encrypted_property(struct connman_provider *provider,
					const char *key, GKeyFile *keyfile)
{
	const char *str = connman_provider_get_string(provider, key);

	if (str != NULL)
		__connman_storage_set_encrypted_value(keyfile,
			provider->identifier, key, str);
}

void connman_provider_load_property(struct connman_provider *provider,
					const char *key, GKeyFile *keyfile)
{
	gchar *str = g_key_file_get_string(keyfile, provider->identifier, key,
						NULL);

	if (str != NULL) {
		connman_provider_set_string(provider, key, str);
		g_free(str);
	}
}

void connman_provider_load_encrypted_property(struct connman_provider *provider,
					const char *key, GKeyFile *keyfile)
{
	gchar *str = __connman_storage_get_encrypted_value(keyfile,
							provider->identifier,
							key);

	if (str != NULL) {
		connman_provider_set_string(provider, key, str);
		g_free(str);
	}
}

gboolean connman_provider_clear_property(struct connman_provider *provider,
							const char *key)
{
	return connman_element_clear_property(&provider->element, key);
}

const char *connman_provider_get_string(struct connman_provider *provider,
							const char *key)
{
	return connman_element_get_string(&provider->element, key);
}

void *connman_provider_get_data(struct connman_provider *provider)
{
	return provider->driver_data;
}

void connman_provider_set_data(struct connman_provider *provider, void *data)
{
	provider->driver_data = data;
}

void connman_provider_set_index(struct connman_provider *provider, int index)
{
	if (provider->vpn_service == NULL)
		return;
	provider->element.index = index;
}

int connman_provider_get_index(struct connman_provider *provider)
{
	return provider->element.index;
}

void connman_provider_set_interface(struct connman_provider *provider,
    const char *interface)
{
	g_free(provider->interface);
	provider->interface = g_strdup(interface);
}

const char *connman_provider_get_interface(struct connman_provider *provider)
{
	return provider->interface;
}

struct provider_input_cb_arg {
	struct connman_provider *provider;
	provider_agent_cb_t callback;
	void *arg;
#define	MAXPARAMS	(2*25)
	const char *params[MAXPARAMS];
};

static int find_param(const char *params[], const char *key)
{
	int i;

	for (i = 0; params[i] != NULL; i += 2) {
		if (g_strcmp0(params[i], key) == 0)
			return i;
	}
	return -1;
}

static int __type_botch(DBusMessageIter *e, const char *func, const char *param)
{
	connman_error("%s: protocol botch; unexpected type %d for %s",
	    func, dbus_message_iter_get_arg_type(e), param);
	return FALSE;
}

static int __copy_back_params(struct connman_provider *provider,
    const char *params[], DBusMessageIter *dict)
{
	while (dbus_message_iter_get_arg_type(dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, variant;
		char *key, *value;
		int ix;

		dbus_message_iter_recurse(dict, &entry);

		if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_STRING)
			return __type_botch(&entry, __func__, "key");
		dbus_message_iter_get_basic(&entry, &key);
		dbus_message_iter_next(&entry);

		if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT)
			return __type_botch(&entry, __func__, "value");
		dbus_message_iter_recurse(&entry, &variant);
		dbus_message_iter_get_basic(&variant, &value);

		_DBG_PROVIDER("key %s value %s", key,
			      connman_log_get_masked_value(key, value));

		ix = find_param(params, key);
		if (ix == -1) {
			connman_error("%s: unknown parameter '%s' ignored",
			    __func__, key);
			goto skip;
		}
		if (g_strcmp0(params[ix+1], value) != 0) {
			/* copy-back new property value */
			connman_provider_set_string(provider,
			    params[ix+1], value);
		}
	skip:
		dbus_message_iter_next(dict);
	}
	return TRUE;
}

static void provider_input_cb(struct connman_service *service, void *arg,
    DBusMessageIter *dict)
{
	struct provider_input_cb_arg *cb_arg = arg;
	struct connman_provider *provider = cb_arg->provider;

	_DBG_PROVIDER("service %p arg %p dict %p", service, arg, dict);

	if (dict != NULL &&
	    __copy_back_params(cb_arg->provider, cb_arg->params, dict)) {
		cb_arg->callback(provider, cb_arg->arg, TRUE);
	} else {
		/* NB: signal error */
		cb_arg->callback(provider, cb_arg->arg, FALSE);
	}

	connman_provider_unref(provider);
	g_free(cb_arg);
}

/*
 * Issue a RequestInput callback to the registered Agent and arrange
 * for a callback on receiving user response(s).  The Agent dbus method
 * includes a dictionary of parameters identifying fields required by
 * the caller.  The flimflam varargs calling convention is to specify
 *    <field-tag, property-name>
 * tuples where a field-tag is passed verbatim in the field's dictionary
 * and the property-name references a provider property who's value is
 * included in the dictionary, and updated on return.  Additional
 * parameters may be included with known values.  For example, to request
 * username and password one might use:
 *
 *   "Username", "OpenVPN.User", "Password", "OpenVPN.Password", NULL
 *
 * and the dictionary passed in the Agent method will include:
 *
 *   <"Username", provider-property("OpenVPN.User")>
 *   <"Password", provider-property("OpenVPN.Password")>
 *
 * (with provider-property("OpenVPN.User") and
 * provider-property("OpenVPN.Password") updated according to the method
 * return data.
 *
 * Callbacks are also given a status parameter that indicates if an error
 * occurred.  Errors range from there not being a registered Agent, to a
 * DBus error (including a Cancel reply by the user).
 *
 * NB: missing property values are converted by the Agent support
 *     to null strings (because there's no way to represent missing).
 * TODO(sleffler) need a way to indicate values should not be displayed
 *     by default (e.g. passwords)
 */
int connman_provider_request_input(struct connman_provider *provider,
    provider_agent_cb_t callback, void *arg, ...)
{
	struct provider_input_cb_arg *cb_arg;
	const char *params[MAXPARAMS];
	va_list ap;
	int i;

	_DBG_PROVIDER("provider %p callback %p arg %p",
	    provider, callback, arg);

	if (provider->vpn_service == NULL) {
		connman_error("%s: no service", __func__);
		return -EINVAL;
	}
	cb_arg = g_try_new0(struct provider_input_cb_arg, 1);
	if (cb_arg == NULL) {
		connman_error("%s: no memory for callback", __func__);
		return -ENOMEM;
	}
	cb_arg->provider = connman_provider_ref(provider);
	cb_arg->callback = callback;
	cb_arg->arg = arg;

	i = 0;
	va_start(ap, arg);
	for (;;) {
		const char *key, *prop_name;

		/* TODO(sleffler) check arg count */

		key = va_arg(ap, const char *);
		if (key == NULL)
			break;
		cb_arg->params[i] = params[i] = key;
		i++;

		prop_name = va_arg(ap, const char *);
		cb_arg->params[i] = prop_name;
		params[i] = connman_provider_get_string(provider, prop_name);
		i++;

		_DBG_PROVIDER("key %s prop %s prop val %s", key, prop_name,
		    params[i-1]);
	}
	params[i] = NULL;
	va_end(ap);

	return __connman_agent_request_input(provider->vpn_service, params,
	    provider_input_cb, cb_arg);
}

static void __connman_provider_ipconfig_init(struct connman_provider *provider)
{
	CONNMAN_ASSERT(provider->ipv4config == NULL);
	provider->ipv4config = connman_ipconfig_create(
	    CONNMAN_IPCONFIG_TYPE_IPV4, "vpn", provider,
	    provider->element.index, TRUE);
	if (provider->ipv4config == NULL) {
		connman_error("%s: could not create ipconfig", __func__);
		/* TODO(sleffler) how to handle this? */
		return;
	}
	connman_ipconfig_register(provider->ipv4config, &provider->element);
}

/*
 * Given an element, search for any associated provider/service.
 * This is used to find the service associated with an ipconfig
 * or connection element.  We cannot use the normal mechanism
 * because we hang ipconfig records directly off the provider
 * (there is no device object).
 */
struct connman_service *__connman_provider_get_service(
    struct connman_element *element)
{
	if (element == NULL)
		return NULL;
	if (element->type == CONNMAN_ELEMENT_TYPE_PROVIDER) {
		/* NB: setup by connman_provider_new */
		struct connman_provider *provider = element->private;
		return provider->vpn_service;
	}
	/* recurse up the tree; we're only called by descendents */
	return __connman_provider_get_service(element->parent);
}

/*
 * Install ipconfig state for use at bind time when the provider
 * reaches the READY state.
 */
void connman_provider_ipconfig_set(struct connman_provider *provider,
					const struct connman_ipaddress *ipaddr)
{
	/* TODO(sleffler) check ipaddr->af to select ipv4/ipv6 */
	if (provider->ipv4config == NULL)
		__connman_provider_ipconfig_init(provider);
	if (provider->ipv4config == NULL) {
		connman_error("%s: no config record setup", __func__);
		return;
	}
	connman_ipconfig_set_address(provider->ipv4config, ipaddr);
}

void connman_provider_ipconfig_bind(struct connman_provider *provider)
{
	if (provider->ipv4config != NULL)
		connman_ipconfig_bind(provider->ipv4config,
		    connman_ipconfig_get_ipaddress(provider->ipv4config));
	/* TODO(sleffler) add ipv6 */
}

void connman_provider_ipconfig_clear(struct connman_provider *provider)
{
	if (provider->ipv4config != NULL) {
		connman_ipconfig_delete(provider->ipv4config);
		provider->ipv4config = NULL;
	}
	/* TODO(sleffler) add ipv6 */
}

/*
 * Add a route to the ipconfig state for use at bind time.
 */
int connman_provider_append_route(struct connman_provider *provider,
					const char *host, const char *netmask,
					const char *gateway)
{
	/* TODO(sleffler) check ipaddr->af to select ipv4/ipv6 */
	if (provider->ipv4config == NULL) {
		connman_error("%s: no config record setup", __func__);
		return FALSE;
	}
	return connman_ipconfig_add_route(provider->ipv4config,
	    host, netmask, gateway);
}

const char *connman_provider_get_driver_name(struct connman_provider *provider)
{
	return provider->driver->name;
}

static gint compare_priority(gconstpointer a, gconstpointer b)
{
	return 0;
}

static void clean_provider(gpointer key, gpointer value, gpointer user_data)
{
	struct connman_provider *provider = value;

	if (provider->driver != NULL && provider->driver->remove)
		provider->driver->remove(provider);
}

int connman_provider_driver_register(struct connman_provider_driver *driver)
{
	_DBG_PROVIDER("driver %p name %s", driver, driver->name);

	driver_list = g_slist_insert_sorted(driver_list, driver,
							compare_priority);
	return 0;
}

void connman_provider_driver_unregister(struct connman_provider_driver *driver)
{
	_DBG_PROVIDER("driver %p name %s", driver, driver->name);

	driver_list = g_slist_remove(driver_list, driver);
}

struct connman_profile *__connman_provider_get_profile(
    struct connman_provider *provider)
{
	return provider->profile;
}

/*
 * Set the provider's bound profile.
 */
void __connman_provider_set_profile(struct connman_provider *provider,
    struct connman_profile *profile)
{
	_DBG_PROVIDER("profile %p -> %p", provider->profile, profile);

	provider->profile = profile;
}

/*
 * Handle a profile being push'd on the stack (e.g. on login).
 * Try to bind any provider objects that are currently unbound.
 */
static void load_profile(gpointer key, gpointer value, gpointer user_data)
{
	struct connman_provider *provider = value;

	if (provider->profile == NULL)
		__connman_profile_load_provider(provider);
}
static void provider_profile_push(struct connman_profile *profile)
{
	_DBG_PROVIDER("profile %p", profile);

	g_hash_table_foreach(provider_hash, load_profile, NULL);
}

static int provider_load(struct connman_provider *provider, GKeyFile *keyfile)
{
	int i;

	_DBG_PROVIDER("provider %p", provider);

	if (g_key_file_has_group(keyfile, provider->identifier) == FALSE)
		return -ESRCH;

	for (i = 0; i < PROVIDER_NTAGS; i++) {
		gchar *str = g_key_file_get_string(keyfile,
		    provider->identifier, provider_tags[i].keyfile_tag, NULL);
		if (str != NULL) {
			connman_provider_set_string(provider,
			    provider_tags[i].keyfile_tag, str);
			g_free(str);
		}
	}

	if (provider->driver != NULL &&
	    provider->driver->load_props != NULL)
		provider->driver->load_props(provider, keyfile);

	return 0;
}

static int provider_save(struct connman_provider *provider, GKeyFile *keyfile)
{
	int i;

	_DBG_PROVIDER("provider %p", provider);

	for (i = 0; i < PROVIDER_NTAGS; i++) {
		const char *str = connman_provider_get_string(provider,
		    provider_tags[i].keyfile_tag);
		if (str != NULL)
			g_key_file_set_string(keyfile, provider->identifier,
			    provider_tags[i].keyfile_tag, str);
	}

	if (provider->driver != NULL &&
	    provider->driver->save_props != NULL)
		provider->driver->save_props(provider, keyfile);

	return 0;
}

static struct connman_storage provider_storage = {
	.name		= "provider",
	.priority	= CONNMAN_STORAGE_PRIORITY_LOW,
	.provider_load	= provider_load,
	.provider_save	= provider_save,
};

static struct connman_notifier provider_notifier = {
	.name		= "provider",
	/* NB: go before service to enable autoconnect */
	.priority	= CONNMAN_NOTIFIER_PRIORITY_DEFAULT+1,
	.profile_push	= provider_profile_push,
};

int __connman_provider_init(void)
{
	_DBG_PROVIDER("");

	connection = connman_dbus_get_connection();

	if (connman_storage_register(&provider_storage) < 0)
		connman_error("Failed to register provider storage");
	if (connman_notifier_register(&provider_notifier) < 0)
		connman_error("Failed to register provider notifier");

	provider_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
						NULL, unregister_provider);

	return 0;
}

void __connman_provider_cleanup(void)
{
	_DBG_PROVIDER("");

	connman_storage_unregister(&provider_storage);
	connman_notifier_unregister(&provider_notifier);

	g_hash_table_foreach(provider_hash, clean_provider, NULL);

	g_hash_table_destroy(provider_hash);
	provider_hash = NULL;

	dbus_connection_unref(connection);
}
